<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>购物车</title>
  <link rel="stylesheet" href="./cat.css" />
</head>

<body>
  <div class="cat">
    <div class="goods-container">
      <div class="goods-nav"></div>
      <div class="goods-list"></div>
    </div>
    <div class="goods-footer">
      <div class="goods-footer-icon">
        <i class="footer-icon"></i>
        <span class="footer-badge"></span>
      </div>
      <div class="goods-footer-prise">
        <p class="prise-number"></p>
        <p class="prise-tip">配送费 ¥0.00</p>
      </div>
      <div class="goods-footer-pay">
        <span></span>
        <a class="pay">去结算</a>
      </div>
    </div>
  </div>

  <script>
    const goods = [
      {
        id: 1,
        title: 'iPhone6 手机',
        pic: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg',
        desc: '描述信息',
        sellNumber: 200,
        price: 19.9,
        favorRate: 96
      },
      {
        id: 2,
        title: 'Mac 电脑',
        pic: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg',
        desc: '描述信息',
        sellNumber: 200,
        price: 19.7,
        favorRate: 93
      },
      {
        id: 3,
        title: 'Mac 平板电脑',
        pic: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-3.jpeg',
        desc: '描述信息',
        sellNumber: 200,
        price: 29.9,
        favorRate: 94
      },
      {
        id: 4,
        title: 'Mac Air 笔记本电脑',
        pic: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-4.jpeg',
        desc: '描述信息',
        sellNumber: 200,
        price: 39.9,
        favorRate: 97
      },
      {
        id: 5,
        title: 'Mac Mini 平板电脑',
        pic: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-5.jpeg',
        desc: '描述信息',
        sellNumber: 200,
        price: 19.1,
        favorRate: 92
      },
      {
        id: 6,
        title: 'Mac Mini2 平板电脑',
        pic: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-6.jpeg',
        desc: '描述信息',
        sellNumber: 200,
        price: 19.88,
        favorRate: 91
      },
      {
        id: 7,
        title: 'Mac 15寸笔记本电脑',
        pic: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-7.jpeg',
        desc: '描述信息',
        sellNumber: 200,
        price: 19.99,
        favorRate: 88
      },
      {
        id: 8,
        title: 'iPhone8 手机',
        pic: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-8.jpeg',
        desc: '描述信息',
        sellNumber: 200,
        price: 19.19,
        favorRate: 91
      }
    ]

    /** 包装商品数据 */
    class UIGoods {
      get totalPrise() {
        return this.choose * this.data.price
      }

      get isChoose() {
        return this.choose > 0
      }

      choose = 0

      /**
       * @param g 单件商品数据
       */
      constructor(g) {
        g = { ...g } // 克隆对象
        Object.freeze(g) // 冻结对象
        Object.defineProperty(this, 'data', {
          get() {
            return g
          },
          set() {
            throw Error('UIGoods 的 data 属性是不可修改的')
          }
        })
        let choose = 0
        Object.defineProperty(this, 'choose', {
          get() {
            return choose
          },
          set(val) {
            choose = val
          }
        })
        Object.seal(this) // 密封属性
      }
    }

    /** 页面数据 */
    class UIData {
      /** 商品数据 */
      uiGoods = []

      /** 商品总价 */
      get totalPrise() {
        return this.uiGoods.reduce((pre, cur) => pre + cur.totalPrise, 0)
      }

      /** 商品总数 */
      get totalChooseNumber() {
        return this.uiGoods.reduce((pre, cur) => pre + cur.choose, 0)
      }

      /** 购物车内是否有商品 */
      get hasGoodsInCat() {
        return !!this.totalChooseNumber
      }

      /** 是否超过起送门槛 */
      get isCrossDeliveryThreshold() {
        return this.totalPrise >= this.deliveryThreshold
      }

      /**
       * @param deliveryThreshold 起送门槛, 默认 30
       * @param deliveryPrise 配送费, 默认5
       */
      constructor(deliveryThreshold = 30, deliveryPrise = 5) {
        /** 起送门槛, 默认 30 */
        this.deliveryPrise = deliveryPrise
        /** 配送费, 默认5 */
        this.deliveryThreshold = deliveryThreshold
        this.uiGoods = goods.map(item => new UIGoods(item))
        Object.freeze(this)
      }

      isChoose(index) {
        return this.uiGoods[index].isChoose
      }

      increase(index) {
        this.uiGoods[index].choose++
      }

      decrease(index) {
        this.uiGoods[index].choose--
      }
    }

    /** 页面对象 */
    class UI {
      uiData

      get doms() {
        return {
          goodsContainer: document.querySelector(
            '.goods-container .goods-list'
          ),
          footerPay: document.querySelector('.goods-footer-pay'),
          cat: document.querySelector('.goods-footer-icon'),
          devicePrise: document.querySelector('.goods-footer .prise-tip'),
          footerPayInSpan: document.querySelector('.goods-footer-pay span'),
          totalPrise: document.querySelector('.goods-footer .prise-number'),
          badge: document.querySelector('.goods-footer .footer-badge')
        }
      }

      jumpTarget = { x: 0, y: 0 }

      constructor() {
        this.uiData = new UIData()
        const rect = this.doms.cat?.getBoundingClientRect()
        Object.defineProperty(this, 'jumpTarget', {
          get() {
            if (!rect) return { x: 0, y: 0 }
            return {
              x: ~~(rect.left + rect.width / 2),
              y: ~~(rect.top + rect.height / 5)
            }
          },
          set() {
            throw Error('jumpTarget 不可以修改')
          }
        })

        this.info()
      }

      info() {
        this.__innerHTML()
        this.updateFooter()
        this.listenEvent()
      }

      __innerHTML() {
        const html = this.uiData.uiGoods
          .map((item, index) => {
            return `
        <div class="goods-item">
          <img src="${item.data?.pic}" alt="${item.data?.title}" class="goods-pic">
          <div class="goods-info">
            <h2 class="goods-title">${item.data?.title}</h2>
            <p class="goods-desc">${item.data?.desc}</p>
            <p class="goods-cell">
              <span>月售${item.data?.sellNumber}</span>
              <span>好评${item.data?.favorRate}%</span>
            </p>
            <div class="goods-confirm">
              <p class="goods-prise">
                <span class="goods-prise-unit">¥</span>
                <b>${item.data?.price}</b>
              </p>
              <div class="goods-btns">
                <i class="i-jianhao" data-index="${index}">-</i>
                <span>${item.choose}</span>
                <i class="i-jiahao" data-index="${index}">+</i>
              </div>
            </div>
          </div>
        </div>
        `
          })
          .join('')
        this.doms.goodsContainer.innerHTML = html
      }

      updateFooter() {
        const total = this.uiData.totalPrise
        // 设置配送费
        const deliveryPrise = this.uiData.deliveryPrise.toFixed(2)
        this.doms.devicePrise.textContent = `还差 ¥${deliveryPrise} 元起送`
        // 设置总价
        if (this.uiData.isCrossDeliveryThreshold) {
          this.doms.footerPay.classList.add('active')
        } else {
          this.doms.footerPay.classList.remove('active')
          const dis = ~~(this.uiData.deliveryThreshold - total)
          this.doms.footerPayInSpan.textContent = `还差 ¥${dis}元起送`
        }
        this.doms.totalPrise.textContent = total.toFixed(2)
        this.doms.cat.classList[this.uiData.hasGoodsInCat ? 'add' : 'remove'](
          'active'
        )
        this.doms.badge.textContent = this.uiData.totalChooseNumber.toFixed(0)
      }

      listenEvent() {
        this.doms.cat.addEventListener('animationend', () => {
          this.doms.cat.classList.remove('animate')
        })
      }

      updateGoodsItem(index) {
        const goodDom = this.doms.goodsContainer.children[index]
        goodDom.classList[this.uiData.isChoose(index) ? 'add' : 'remove'](
          'active'
        )
        const span = goodDom.querySelector('.goods-btns span')
        span.textContent = this.uiData.uiGoods[index].choose.toFixed(0)
      }

      /**
       * 抛物线跳跃元素
       * @param index
       */
      jump(index) {
        const rect = this.doms.goodsContainer.children[index]
          .querySelector('.i-jiahao')
          .getBoundingClientRect()
        const start = {
          x: ~~(rect.left - rect.width / 2),
          y: ~~(rect.top - rect.height / 2)
        }
        const div = document.createElement('div')
        const span = document.createElement('span')
        div.className = 'add-to-cat'
        span.className = 'add-to-cat-icon'
        span.textContent = '+'
        div.style.transform = `translateX(${start.x}px)`
        span.style.transform = `translateY(${start.y}px)`
        div.appendChild(span)
        document.body.append(div)
        let time = Date.now()
        const animationFrame = () => {
          div.style.transform = `translateX(${this.jumpTarget.x}px)`
          span.style.transform = `translateY(${this.jumpTarget.y}px)`

          if (Date.now() - time < 5000) {
            window.requestAnimationFrame(animationFrame)
          }
        }
        requestAnimationFrame(animationFrame)

        span.addEventListener(
          'transitionend',
          () => {
            div.remove()
            this.carAnimate()
          },
          { once: true }
        )
      }

      carAnimate() {
        this.doms.cat.classList.add('animate')
      }

      increase(index) {
        this.uiData.increase(index)
        this.updateGoodsItem(index)
        this.updateFooter()
        this.jump(index)
      }

      decrease(index) {
        this.uiData.decrease(index)
        this.updateGoodsItem(index)
        this.updateFooter()
      }
    }

    const ui = new UI()

    ui.doms.goodsContainer.addEventListener('click', e => {
      const classList = e.target.classList
      const { index = '0' } = e.target.dataset
      if (classList.contains('i-jianhao')) {
        ui.decrease(~~index)
      } else if (classList.contains('i-jiahao')) {
        ui.increase(~~index)
      }
    })
  </script>
</body>

</html>